package com.cn.gmall.common.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cn.gmall.common.mqconst.MqConst;
import com.cn.gmall.common.pojo.GmallCorrelationData;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;


import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;


@Component
@Slf4j
public class MqConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 修饰一个非静态的void（）方法,
     * 在服务器加载Servlet的时候运行，
     * 并且只会被服务器执行一次在构造函数之后执行，
     * init（）方法之前执行。
     */
    /**
     * @PostConstruct注解的原理大致， * 在Spring容器刷新创建bean实例时会构建bean生命周期元数据，
     * * 在元数据中会保存@PostConstruct和@PreDestroy注解修饰的方法，
     * * 然后在bean属性赋值阶段会进行bean的依赖注入检查如果依赖的bean没有被创建则会创建依赖的bean并进行依赖注入
     * * ，最后在bean的初始化过程中会执行postProcessBeforeInitialization方法，
     * * 该方法在bean初始化之前执行，
     * * postProcessBeforeInitialization该方法会通过bean的声明周期元数据通过反射进行方法调用，
     * * 这就实现了@PostConstruct注解的原理
     * *
     */
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    /***
     *   如果消息没有到exchange,则confirm回调,ack=false
     *   如果消息到达exchange,则confirm回调,ack=true
     * @param correlationData  发送消息 可以携带的对象
     * @param
     * @param
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {

        if (ack) {
            log.info("消息到交换机成功->" + JSON.toJSONString(correlationData));
        } else {
            log.info("消息到交换机失败->" + cause + "数据:" + JSON.toJSONString(correlationData));
//        重试提交
            this.retrySendMsg(correlationData);

        }
    }

    /**
     * 重试机制(普通消息 和延时消息 )
     *
     * @param correlationData
     */
    private void retrySendMsg(CorrelationData correlationData) {
//        从父类转子类
        GmallCorrelationData gmallCorrelationData = (GmallCorrelationData) correlationData;
//    获取重试次数 默认是0
        int retryCount = gmallCorrelationData.getRetryCount();
        if (retryCount >= MqConst.RETRY_COUNT) {
//            重试次数已达上限
            log.error("重试次数已达上限,发送消息失败->" + JSON.toJSONString(gmallCorrelationData));

        } else {
            retryCount++;
            System.out.println("第" + retryCount + "次 ,尝试发送消息");
            gmallCorrelationData.setRetryCount(retryCount); //更新重试次数

//          保存到redis
            redisTemplate.opsForValue().set(MqConst.MQ_KEY_PREFIX + gmallCorrelationData.getId(),
                    JSONObject.toJSONString(gmallCorrelationData),
                    30,
                    TimeUnit.MINUTES);

//            判断是否是延时消息
            if (gmallCorrelationData.isDelay()) {
//isDelay 存在表示延时消息
                this.rabbitTemplate.convertAndSend( //重新发送消息
                        gmallCorrelationData.getExchange(),
                        gmallCorrelationData.getRoutingKey(),
                        gmallCorrelationData.getMessage(),
                        message -> {
                            message.getMessageProperties().setDelay(gmallCorrelationData.getDelayTime()*1000);
                       return message;
                        },
                        gmallCorrelationData
                );
            }else { //普通消息

/***     发送消息
 * exchange: 这是一个字符串，
 * 表示消息要发送到的交换机（exchange）的名称。
 * 交换机是 RabbitMQ 的组件，
 * 它用于接收并路由消息到一个或多个队列中。
 *
 * routingKey: 这是一个字符串，
 * 表示消息要发送到的队列的路由键（routing key）。
 * 路由键用于交换机将消息路由到特定的队列。
 *
 * message: 这是要发送的消息对象，
 * 以是任何 Java 对象。
 * rabbitTemplate
 * 将根据配置的消息转换器将消息对象转换为 RabbitMQ 可接受的消息格式。
 *
 * correlationData:
 * 这是一个对象，用于关联发送的消息和接收到的消息的相关数据。
 * 通常用于实现消息的可靠性确认（publisher confirm）。
 *
 * rabbitTemplate.convertAndSend 方法接受上述参数，
 * 并将消息发送到指定的交换机和队列。它允许你方便地将消息发送到 RabbitMQ，
 * 而无需手动处理底层的连接和通信细节。
 */
                rabbitTemplate.convertAndSend(gmallCorrelationData.getExchange(),
                        gmallCorrelationData.getRoutingKey(),
                        gmallCorrelationData.getMessage(),
                        gmallCorrelationData);
            }

        }


    }

    /***
     * exchange到queue成功,则不回调return
     *      *   exchange到queue失败,则回调return
     * @param message  消息对象
     * @param replyCode 应答码
     * @param replyText 描述
     * @param exchange  消息使用的交换器 exchange
     * @param routingKey 消息使用的路由键 routing :
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        // 反序列化对象输出
        System.out.println("消息主体: " + JSON.toJSONString(message.getBody()));
        System.out.println("应答码: " + replyCode);
        System.out.println("描述：" + replyText);
        System.out.println("消息使用的交换器 exchange : " + exchange);
        System.out.println("消息使用的路由键 routing : " + routingKey);
//根据对象回去id
        String correlationId = (String) message.getMessageProperties().getHeaders().get("spring_returned_message_correlation");

        //redis里面找
        String corrJson = (String) redisTemplate.opsForValue().get(MqConst.MQ_KEY_PREFIX + correlationId);

//转换类型
        if (!StringUtils.isEmpty(corrJson)) {

            GmallCorrelationData gmallCorrelationData = JSONObject.parseObject(corrJson, GmallCorrelationData.class);
            this.retrySendMsg(gmallCorrelationData);
        }


    }
}
